package com.zsp.coupon.service.imp;


import com.alibaba.fastjson.JSON;
import com.zsp.coupon.constant.Constant;
import com.zsp.coupon.constant.CouponStatus;
import com.zsp.coupon.entity.Coupon;
import com.zsp.coupon.exception.CouponException;
import com.zsp.coupon.service.IRedisService;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;


import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class RedisServiceImp implements IRedisService {
// redis客户端
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * <h2>根据 userId 和状态找到缓存的优惠券列表数据</h2>
     * @param userId 用户 id
     * @param status 优惠券状态 {@link com.zsp.coupon.constant.CouponStatus}
     * @return {@link Coupon}s, 注意, 可能会返回 null, 代表从没有过记录
     * */
    @Override
    public List<Coupon> getCacheCoupons(Long userId, Integer status) {

        log.info("Get Coupon from Cache :{},{}",userId,status);

        /**
         * 自己写的方法status2RedisKey，依据这两个参数找到rediskey
         */
        String redisKey=status2RedisKey(status,userId);

        List<String>  couponStrs = redisTemplate
                .opsForHash().values(redisKey)
                .stream().map(o -> Objects.toString(o,null))
                .collect(Collectors.toList());
        //利用集合工具类判断是否为空
        if (CollectionUtils.isEmpty(couponStrs))
        {
            saveEmptyCouponListToCache(userId,
                    Collections.singletonList(status));
            return Collections.emptyList();
        }
        return couponStrs.stream().map(cs ->  JSON.parseObject(cs ,Coupon.class))
                .collect(Collectors.toList());
    }

    /**
     * <h2>保存空的优惠券列表到缓存中</h2>
     * 目的：避免缓存穿透
     * @param userId 用户 id
     * @param status 优惠券状态列表
     * */
    @Override
    @SuppressWarnings("all")
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {
            log.info("save empty list to cache For user；{}，{}",userId, JSON.toJSONString(status));
//      key 时coupon_id value 是序列化的coupon
              Map<String,String> invaildCouponMap=new HashMap<>();
              invaildCouponMap.put("-1",JSON.toJSONString(Coupon.invalidCoupon()));
        /**
         * 用户优惠卷缓存信息
         * kv  k status ->rediskey
         * v:{coupon_id 序列化的Coupon}
         */

        //使用sessionCallback 把数据命令放入到Redis的pipeline
        SessionCallback<Object> sessionCallback=new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                status.forEach(s ->{
                    String redisKey=status2RedisKey(s,userId);
                    operations.opsForHash().putAll(redisKey,invaildCouponMap);
                });
                return null;
            }
        };
        log.info("Pipleline Exe  Result :{}",
                JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));
    }

    /**
     * <h2>尝试从 Cache 中获取一个优惠券码</h2>
     * @param templateId 优惠券模板主键
     * @return 优惠券码
     * */

    @Override
    public String tryToAcquireCouponCodeFromCache(Integer templateId) {
        String redisKey = String.format("%s%s",
                Constant.RedisPrefix.COUPON_TEMPLATE,templateId.toString());
        //因为优惠卷码不存在顺序关系 左边pop 或者右边pop都i没有影响
        String couponCode = redisTemplate.opsForList().leftPop(redisKey);
        log.info("Acquire Coupon  code:{},{},{}",templateId,redisKey,couponCode);
        return couponCode;
    }

    /**
     * <h2>将优惠券保存到 Cache 中</h2>
     * @param userId 用户 id
     * @param coupons {@link Coupon}s
     * @param status 优惠券状态
     * @return 保存成功的个数
     * */
    @Override
    public Integer addCouponToCache(Long userId,
                                    List<Coupon> coupons,
                                    Integer status) throws CouponException {
        log.info("Add coupon To Cache :{},{},{}",userId,coupons,status);
        Integer result=-1;
        CouponStatus couponStatus=CouponStatus.of(status);
        switch (couponStatus){
            case USEABLE:
                result = addCouponToCacheForUseable(userId,coupons);
                break;
            case USED:
                result = addCouponToCacheForUsed(userId,coupons);
                break;
            case EXPIRED:
                result = addCouponToCacheForExpired(userId,coupons);
                break;
        }
        return result;
    }

    /**
     * 新增加优惠卷到cache中
     * @param userId
     * @param coupons
     * @return
     */
    private Integer addCouponToCacheForUseable(Long userId,List<Coupon> coupons){
            //如果status是userable，代表是新增加的优惠卷
//        指挥影响一个cache USER_COUPON_USABLE
        log.debug("Add Coupon to cache for usable");
        Map<String ,String> needCacheObject = new HashMap<>();
        coupons.forEach(c -> needCacheObject
                .put(c.getId().toString(),
                JSON.toJSONString(c)));

        String redisKey = status2RedisKey(CouponStatus.USEABLE.getCode(),userId);
        redisTemplate.opsForHash().putAll(redisKey,needCacheObject);
        log.info("Add {} Coupons To Cache :{},{}",
                needCacheObject.size(),userId,redisKey);

        redisTemplate.expire(redisKey,
                getRandomExpireationTime(1,2),
                TimeUnit.SECONDS);
        return needCacheObject.size();
    }

    /**
     * 将已经使用的优惠卷加入到cache中
     * @param useId
     * @param coupons
     * @return
     */
    @SuppressWarnings("all")
    private  Integer addCouponToCacheForUsed(Long useId,List<Coupon>coupons) throws CouponException {
//        // 如果 status 是 USED, 代表用户操作是使用当前的优惠券, 影响到两个 Cache
        log.debug("Add Coupon To Cache For Used.");

        Map<String,String> needCachedForUsed=new HashMap<>(coupons.size());
            String redisKeyForUsable=status2RedisKey(
                    CouponStatus.USEABLE.getCode(),
                    useId);

        String redisKeyForUsed = status2RedisKey(
                CouponStatus.USED.getCode(),
                useId);
        // 获取当前用户可用的优惠券
        List<Coupon> curUsableCoupons = getCacheCoupons(
                useId,
                CouponStatus.USEABLE.getCode()
        );

        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();

        coupons.forEach(c -> needCachedForUsed.put(
            c.getId().toString(),
            JSON.toJSONString(c)
        ));

        // 校验当前的优惠券参数是否与 Cached 中的匹配

            List<Integer> curUsableIds =curUsableCoupons.stream()
                    .map(Coupon::getId).collect(Collectors.toList());
            List<Integer> parmsId =coupons.stream()
                    .map(Coupon::getId).collect(Collectors.toList());

            if (! CollectionUtils.isSubCollection(parmsId,curUsableIds)) {
                log.error("CurCoupons Is Not Equal ToCache: {}, {}, {}",
                        useId,JSON.toJSONString(curUsableIds),
                        JSON.toJSONString(parmsId));
                throw new CouponException("CurCoupons Is Not Equal To Cache!");
            }
            List<String> needCleanKey = parmsId.stream()
                    .map(i -> i.toString()).collect(Collectors.toList());
            SessionCallback<Objects> sessionCallback = new SessionCallback<Objects>() {
                @Override
                public  Objects execute(RedisOperations operations) throws DataAccessException {

                    // 1. 已使用的优惠券 Cache 缓存添加
                        operations.opsForHash().putAll(
                                redisKeyForUsed,needCachedForUsed
                        );
                    // 2. 可用的优惠券 Cache 需要清理
                        operations.opsForHash().delete(
                                redisKeyForUsable,needCleanKey.toArray()
                        );

                    // 3. 重置过期时间

                        operations.expire(redisKeyForUsable,
                                getRandomExpireationTime(1,2),
                                TimeUnit.SECONDS);
                        operations.expire(redisKeyForUsed,
                                getRandomExpireationTime(1,2),
                                TimeUnit.SECONDS);
                    return null;
                }
            };
            log.info("Pipeline Exe Result: {}",
                    JSON.toJSONString(
                            redisTemplate.executePipelined(sessionCallback)
                    ));
            return coupons.size();
    }

    /**
     * <h2>将过期优惠券加入到 Cache 中</h2>
     * */
    @SuppressWarnings("all")
    private Integer addCouponToCacheForExpired(Long userId,List<Coupon> copons)
            throws CouponException{
// status 是 EXPIRED, 代表是已有的优惠券过期了, 影响到两个 Cache
        // USABLE, EXPIRED

        log.debug("Add Coupon To Cache For Expired.");

        // 最终需要保存的 Cache
       Map<String,String> needCachedForExpired = new HashMap<>(copons.size());

       String redisKeyForUsable = status2RedisKey(
               CouponStatus.USEABLE.getCode(),userId
       );


        String redisKeyForExpired = status2RedisKey(
                CouponStatus.EXPIRED.getCode(), userId
        );


        List<Coupon> curUsableCoupons =getCacheCoupons(userId,
                CouponStatus.USEABLE.getCode());


        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > copons.size();

        copons.forEach(c -> needCachedForExpired.put(
                c.getId().toString(),
                JSON.toJSONString(c)
        ));

        // 校验当前的优惠券参数是否与 Cached 中的匹配

        List<Integer> curUsableIds =curUsableCoupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());

        List<Integer> paramsIds = copons.stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());

        if (!CollectionUtils.isSubCollection(paramsIds,curUsableIds)){
            log.error("CurCoupons Is Not Equal To Cache: {}, {}, {}",
                    userId, JSON.toJSONString(curUsableIds),
                    JSON.toJSONString(paramsIds));
            throw new CouponException("CurCoupon Is Not Equal To Cache.");
        }

        List<String> needCleanKey = paramsIds.stream()
                .map(i ->i.toString())
                .collect(Collectors.toList());

        SessionCallback<Objects> sessionCallback = new SessionCallback<Objects>() {
            @Override
            public  Objects execute(RedisOperations operations) throws DataAccessException {
                // 1. 已过期的优惠券 Cache 缓存
                operations.opsForHash().putAll(
                        redisKeyForExpired, needCachedForExpired
                );
                // 2. 可用的优惠券 Cache 需要清理
                operations.opsForHash().delete(
                        redisKeyForUsable, needCleanKey.toArray()
                );
                // 3. 重置过期时间
                operations.expire(
                        redisKeyForUsable,
                        getRandomExpireationTime(1, 2),
                        TimeUnit.SECONDS
                );
                operations.expire(
                        redisKeyForExpired,
                        getRandomExpireationTime(1, 2),
                        TimeUnit.SECONDS
                );

                return null;
            }
        };
        log.info("Pipeline Exe Result: {}",
                JSON.toJSONString(
                        redisTemplate.executePipelined(sessionCallback)
                ));
        return  copons.size();
    }


    //根据status获取到对应Redis key
    private String status2RedisKey(Integer status,Long userId){
            String redisKey=null;
        CouponStatus couponStatus= CouponStatus.of(status);
         switch (couponStatus){
             case USEABLE:
                 redisKey = String.format("%s%s",
                         Constant.RedisPrefix.USER_COUPON_USABLE,userId );
                  break;
             case USED:
                 redisKey= String.format("%s%s",
                     Constant.RedisPrefix.USER_COUPON_USED,userId);
                  break;

             case EXPIRED:
                 redisKey = String.format("%s%s",
                         Constant.RedisPrefix.USER_COUPON_EXPIRED, userId);
                 break;
         }
        return redisKey;
    }

    /**
     * 获取一个随机过期的时间
     * 缓存雪崩：key在同一时间失效
     * min：
     * max：
     * @return 返回[min,max]之间的随机的秒数
     */
    private Long getRandomExpireationTime(Integer min,Integer max){

        return RandomUtils.nextLong(
                min *60 * 60,
                max * 60 * 60);
    }

}
